package org.makumba.providers.query.mql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.makumba.DataDefinition;
import org.makumba.FieldDefinition;
import org.makumba.MakumbaError;
import org.makumba.commons.TextList;
import org.makumba.providers.DataDefinitionProvider;
import org.makumba.providers.QueryAnalysis;
import org.makumba.providers.QueryAnalysisProvider;
import org.makumba.providers.QueryAnalysisProvider.ASTTransformVisitor;
import org.makumba.providers.QueryParameters;
import org.makumba.providers.QueryProvider;

import antlr.collections.AST;

/**
 * The hearth of the MQL query analysis and compilation. The query is first pre-processed and then the initial parsing
 * takes place to produce the initial mql tree. After this, the tree is transformed by the MqlSqlWalker for analysis and
 * finally transformed again for sql query generation.
 * 
 * @author Cristian Bogdan
 * @author Manuel Bernhardt <manuel@makumba.org>
 * @version $Id: MqlQueryAnalysis.java,v 1.1 Apr 29, 2009 8:54:20 PM manu Exp $
 */
public class MqlQueryAnalysis implements QueryAnalysis {

    private String query;

    protected DataDefinition insertIn;

    private DataDefinition proj;

    private boolean noFrom = false;

    private LinkedHashMap<String, DataDefinition> labels;

    private Hashtable<String, String> aliases;

    private MqlSqlWalker analyser;

    private AST pass1;

    // all the labels that were generated by this analyser, kept to avoid collisions
    protected Vector<String> generatedLabels = new Vector<String>();

    private List<String> paths;

    private MqlQueryParameters queryParams = new MqlQueryParameters();

    private LinkedHashMap<String, Object> constantValues;

    static String formatQueryAndInsert(String query, String insertIn) {
        if (insertIn != null && insertIn.length() > 0) {
            return "###" + insertIn + "###" + query;
        } else {
            return query;
        }
    }

    /**
     * constructs a query analysis given the pass1 AST tree and a set of known labels (used for MDD function parameters)
     * 
     * @param pass1
     *            the pass1 tree
     * @param knownLabels
     *            the known labels
     */
    public MqlQueryAnalysis(AST pass1, DataDefinition knownLabels) {
        // TODO: the original query should be passed, if one exists, otherwise we can make one like so:
        // query= Pass1ASTPrinter.printAST(pass1).toString();
        init(false, false, pass1, knownLabels);

        pass1 = null;
    }

    MqlQueryAnalysis(String queryAndInsert, boolean optimizeJoins, boolean autoLeftJoin) {
        Date d = new Date();

        if (queryAndInsert.startsWith("###")) {
            insertIn = DataDefinitionProvider.getInstance().getDataDefinition(
                queryAndInsert.substring(3, queryAndInsert.indexOf('#', 3)));
            query = queryAndInsert.substring(queryAndInsert.indexOf('#', 3) + 3);
        } else {
            query = queryAndInsert;
        }

        query = QueryAnalysisProvider.checkForFrom(query);

        AST parsed = QueryProvider.getQueryAnalzyer("oql").inlineFunctions(query);

        noFrom = QueryAnalysisProvider.reduceDummyFrom(parsed);

        /*
         * } else { if (!Configuration.getQ ueryInliner().equals("tree")) { HqlParser parser = null; try { parser =
         * HqlParser.getInstance(query); parser.statement(); } catch (Throwable t) { doThrow(t, parser != null ?
         * parser.getAST() : null); } doThrow(parser.error, parser.getAST()); parsed = parser.getAST(); } else { parsed
         * = FunctionInliner.inlineQueryTree(query); } }
         */

        init(optimizeJoins, autoLeftJoin, parsed, null);

        long diff = new java.util.Date().getTime() - d.getTime();
        java.util.logging.Logger.getLogger("org.makumba.db.query.compilation").fine(
            "MQL analysis: " + diff + " ms: " + query);
        // analyserTreeOriginal = null;
        pass1 = null;
    }

    private void init(boolean optimizeJoins, boolean autoLeftJoin, AST pass1, DataDefinition knownLabels)
            throws MakumbaError {
        this.pass1 = pass1;
        AST parsed = new HqlASTFactory().dupTree(pass1);

        // we need to do the transformation first so the second-pass parser will accept the query
        QueryAnalysisProvider.transformOQLParameters(parsed, queryParams.parameterOrder);
        QueryAnalysisProvider.transformOQL(parsed);

        MqlSqlWalker mqlAnalyzer = new MqlSqlWalker(query, insertIn, optimizeJoins, autoLeftJoin, knownLabels);
        analyser = mqlAnalyzer;
        try {
            mqlAnalyzer.statement(parsed);
        } catch (Throwable e) {
            QueryAnalysisProvider.doThrow(query, e, parsed);
        }
        QueryAnalysisProvider.doThrow(query, mqlAnalyzer.error, parsed);

        labels = mqlAnalyzer.rootContext.labels;
        aliases = mqlAnalyzer.rootContext.aliases;
        paths = new ArrayList<String>(mqlAnalyzer.rootContext.paths.values());
        queryParams.paramInfo = DataDefinitionProvider.getInstance().getVirtualDataDefinition("Parameters for " + query);

        proj = DataDefinitionProvider.getInstance().getVirtualDataDefinition("Projections for " + query);
        mqlAnalyzer.setProjectionTypes(proj);

        // TODO: nofrom queries should only have constant projections
        queryParams.process(analyser, getQuery());

        constantValues = analyser.constantValues;
    }

    /**
     * Two operations: (1) eliminate the subquery correlation filter conditions (2) surround non-case boolean
     * projections with case when true false
     */
    public void prepareForHQL() {

        new ASTTransformVisitor(false) {
            @Override
            public AST visit(AST a) {

                if (getPath().size() == 0) {
                    return a;
                }
                AST parent = getPath().get(getPath().size() - 1);
                if (a.getType() == HqlSqlTokenTypes.FILTERS) {
                    AST ret = a.getNextSibling();
                    if (ret == null) {
                        ret = analyser.getASTFactory().create(HqlSqlTokenTypes.NUM_INT, "1");
                    }
                    return ret;
                }
                FieldDefinition type = ((MqlNode) a).getMakType();

                // we look for boolean projections whose father is the SELECT clause and which are not already CASE
                // statements
                if (type == null || !type.getType().equals("boolean") || !parent.getText().equals("{select clause}")
                        || a.getType() == HqlSqlTokenTypes.CASE || a.getType() == HqlSqlTokenTypes.DOT) {
                    return a;
                }

                AST ret = analyser.getASTFactory().create(HqlSqlTokenTypes.CASE, "case");
                AST when = analyser.getASTFactory().create(HqlSqlTokenTypes.WHEN, "when");

                when.setFirstChild(analyser.getASTFactory().create(a));
                when.getFirstChild().setFirstChild(a.getFirstChild());
                when.getFirstChild().setNextSibling(analyser.getASTFactory().create(HqlSqlTokenTypes.TRUE, "true"));

                AST elze = analyser.getASTFactory().create(HqlSqlTokenTypes.ELSE, "else");
                elze.setFirstChild(analyser.getASTFactory().create(HqlSqlTokenTypes.FALSE, "false"));
                when.setNextSibling(elze);

                ret.setFirstChild(when);
                ret.setNextSibling(a.getNextSibling());
                return ret;
            }

            @Override
            protected AST makeASTcopy(AST a) {
                AST b = analyser.getASTFactory().create(a);
                b.setFirstChild(a.getFirstChild());
                return b;
            }

        }.traverse(analyser.getAST());
    }

    @Override
    public String getQuery() {
        return query;
    }

    @Override
    public DataDefinition getLabelType(String labelName) {
        String s1 = aliases.get(labelName);
        if (s1 != null) {
            labelName = s1;
        }
        return labels.get(labelName);
    }

    @Override
    public Map<String, DataDefinition> getLabelTypes() {
        return labels;
    }

    @Override
    public QueryParameters getQueryParameters() {
        return queryParams;
    }

    @Override
    public DataDefinition getProjectionType() {
        return proj;
    }

    // FIXME this is ugly, there is for sure a way to get the FROM from the tree
    private String getFrom() {

        String[] splitAtFrom = query.split("\\s[f|F][r|R][o|O][m|M]\\s");
        String[] splitAtWhere = splitAtFrom[1].split("\\s[w|W][h|H][e|E][r|R][e|E]\\s");

        return splitAtWhere[0];
    }

    // FIXME needed for relation miner, but should maybe be moved, it's dependent on the analysis per se
    public String getFieldOfExpr(String expr) {
        if (expr.indexOf(".") > -1) {
            return expr.substring(expr.lastIndexOf(".") + 1);
        } else {
            return expr;
        }
    }

    // FIXME needed for relation miner, but should be refactored or made more robust
    public DataDefinition getTypeOfExprField(String expr) {

        if (expr.indexOf(".") == -1) {
            return getLabelType(expr);
        } else {
            DataDefinition result;
            int lastDot = expr.lastIndexOf(".");
            String beforeLastDot = expr.substring(0, lastDot);
            if (beforeLastDot.indexOf(".") == -1) {
                result = getLabelType(beforeLastDot);
            } else {
                // compute dummy query for determining pointed type

                // FIXME will this work if the FROM contains subqueries?
                String dummyQuery = "SELECT " + beforeLastDot + " AS projection FROM " + getFrom();
                result = QueryProvider.getQueryAnalzyer("OQL").getQueryAnalysis(dummyQuery).getProjectionType().getFieldDefinition(
                    "projection").getPointedType();
            }
            return result;

        }
    }

    public static String showAst(AST ast) {
        return ast.toStringTree();
    }

    @Override
    public List<String> getPaths() {
        // TODO: for now this only contains the pointers, not all the fields selected!
        return paths;
    }

    /**
     * Some queries like "SELECT actor(Type)" or "SELECT 1" have no FROM section. Such queries can be optimized so they
     * are not sent to the db engine if they only select constants. Other queries like "SELECT actor(Type).field" don't
     * have a FROM section but will get one after inlining. They must be sent to the db engine.
     * 
     * @return whether the inlined query had no FROM section
     */
    public boolean getNoFrom() {
        return noFrom;
    }

    private static final class MqlQueryParameters implements QueryParameters {
        Set<Integer> multiValueParams;

        DataDefinition paramInfo;

        List<String> parameterOrder = new ArrayList<String>();

        @Override
        public DataDefinition getParameterTypes() {
            return paramInfo;
        }

        @Override
        public List<String> getParameterOrder() {
            return parameterOrder;
        }

        @Override
        public boolean isMultiValue(int position) {
            return multiValueParams.contains(position);
        }

        void process(MqlSqlWalker mqlAnalyzer, String query) {
            multiValueParams = mqlAnalyzer.multiValueParams;

            for (int i = 0; i < parameterOrder.size(); i++) {
                // first we check whether we have a parameter type for exactly this position
                FieldDefinition fd = mqlAnalyzer.paramInfoByPosition.getFieldDefinition("param" + i);

                // failing that, we see if we have a parameter with the same name on another position
                // but we consider that only if the parameter is not multi-type
                if (fd == null && !mqlAnalyzer.multiTypeParams.contains(parameterOrder.get(i))) {
                    fd = mqlAnalyzer.paramInfoByName.getFieldDefinition(parameterOrder.get(i));
                }

                // if we still don't know who this guy is, maybe it's an actor parameter that we generated on the fly
                // FIXME: this can only happen if the parameter is a projection, in which case it was already fixed in
                // MqlSqlWalker#setProjectionTypes()
                if (parameterOrder.get(i).startsWith("actor_")) {
                    String type = parameterOrder.get(i).substring(6).replaceAll("_", ".");
                    fd = mqlAnalyzer.ddp.getDataDefinition(type).getFieldDefinition(0);
                }

                if (fd != null) {
                    // FIXME storing whether or not this field is a multiTypeParam in the *description* field of the
                    // FieldDefinition is an extremely dirty hack!
                    paramInfo.addField(DataDefinitionProvider.getInstance().makeFieldWithName("param" + i, fd,
                        String.valueOf(mqlAnalyzer.multiTypeParams.contains(parameterOrder.get(i)))));
                } else {
                    throw new MakumbaError("Panic: could not compute type of parameter at position " + i
                            + " with name '" + parameterOrder.get(i) + "' of query " + query);
                }

                // now we can translate the name back to the original name (e.g. $param0 -> $1)
                parameterOrder.set(i, QueryAnalysisProvider.getActualParameterName(parameterOrder.get(i)));
            }

        }

    }

    public static class ParamConstant {
        String paramName;

        public ParamConstant(String name) {
            paramName = name;
        }

        public String getParamName() {
            return paramName;
        }
    }

    /**
     * If the inlined query has no FROM, and all its projections are constants, its result is only made of constants.
     * 
     * @return a map with constants if the inlined query has no FROM, and only constant projections, null otherwise
     */
    public Map<String, Object> getConstantValues() {
        return noFrom ? constantValues : null;
    }

    @Override
    public AST getPass1Tree() {
        return pass1;
    }

    @Override
    public Collection<String> getWarnings() {
        return analyser.warnings;
    }

    public TextList getText(MqlSqlGenerator mg) {

        try {
            mg.statement(analyser.getAST());
        } catch (Throwable e) {
            QueryAnalysisProvider.doThrow(getQuery(), e, analyser.getAST());
        }
        QueryAnalysisProvider.doThrow(getQuery(), mg.error, analyser.getAST());

        return mg.getText();
    }
}
